package com.wzw.springboot.principle.transaction.service;

import com.wzw.springboot.principle.aware.CustomApplicationContext;
import com.wzw.springboot.principle.transaction.entity.User;
import com.wzw.springboot.principle.transaction.repository.UserRepository;
import org.springframework.aop.framework.AopContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import javax.annotation.Resource;
import java.sql.ResultSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * Spring内部方法调用，事务失效场景
 * 1.updateUser无事务，addUser有事务
 *   此时addUser内不的事务会失效(本来就没有事务注解)，因为updateUser方法没有生成事务的代理对象。内部直接调用addUser时就不是代理对象了
 *   解决方式：这里是解决子事务addUser不生效的问题，主事务updateUser是没有被事务控制
 *           1.把adduser放在另外一个被代理的Bean对象中，保证调用这个方法的对象是代理对象
 *           2.使用BeanFactory获取UserService对象，再去调用addUser方法
 *           3.开启expose-proxy配置，使用AopContext类获取当前对象，再去调用addUser方法
 * 2.updateUser有事务，addUser无事务
 *   此时两个方法都会有事务控制被回滚了，因为updateUser方法生成了事务代理对象。在执行目标方法时，会被事务包括。目标方法即包括了方法addUser
 * 3.updateUser有事务，addUser有事务
 *   此时两个方法都会有事务控制被归滚了
 * 4.spring默认只会对RuntimeException类型的异常会自动回滚
 *
 * 同理对Spring生成的所有代理对象，都有此类问题。本质上还是只有当前对象是代理对象时，才能执行代理的增强逻辑。如：异步注解 @Async
 * spring单例对象中，如果某个Bean是代理对象。那么在注入或者使用BeanFactory获取时，拿到的也就是代理对象。
 * 代理对象执行方法时，其实相当于去执行了代理增强的方法。就和代理模式实现过程一样。
 * 先去获取被代理的目标原对象 targetObject，在执行 advice 增强方法方法，再去执行目标方法。
 * 当执行目标方法时（调用真正的由开发人员编写的代码），就不是代理对象了。而是目标对象，所以在内部再去调用其他会被代理的方法。就不会生效了，因为不是代理对象调用的。无法执行增强方法。
 *
 * 事务特性：ACID
 *   原子性（Atomic）：表示将事务中所进行的操作捆绑成一个不可分割的单元，即对事务所进行的数据修改等操作，要么全部执行，要么全都不执行。
 *   一致性（Consistency）：表示事务完成时，必须使所有的数据都保持一致状态。
 *   隔离性（Isolation）：指一个事务的执行不能被其他事务干扰，即一个事务内部的操作及使用的数据对并发的其他事务是隔离的，并发执行的各个事务之间不能互相干扰。
 *   持久性（Durability）：持久性也称永久性（permanence），指一个事务一旦提交，它对数据库中的数据的改变就应该是永久性的。提交后的其他操作或故障不会对其有任何影响。
 *
 * 事务隔离级别：
 *   脏读：指一个事务读取到另一个事务未提交的数据。
 *   不可重复读：指一个事务对同一行数据重复读取两次，但得到的结果不同。
 *   幻读：指一个事务执行两次查询，但第二次查询的结果包含了第一次查询中未出现的数据。
 *   丢失更新：指两个事务同时更新一行数据，后提交（或撤销）的事务将之前事务提交的数据覆盖了。
 *
 *   Read Uncommitted（读未提交）：
 *        一个事务在执行过程中，既可以访问其他事务未提交的新插入的数据，又可以访问未提交的修改数据。如果一个事务已经开始写数据，则另外一个事务不允许同时进行写操作，但允许其他事务读此行数据。此隔离级别可防止丢失更新。
 *   Read Committed（读已提交）：
 *        一个事务在执行过程中，既可以访问其他事务成功提交的新插入的数据，又可以访问成功修改的数据。读取数据的事务允许其他事务继续访问该行数据，但是未提交的写事务将会禁止其他事务访问该行。此隔离级别可有效防止脏读。
 *   Repeatable Read（可重复读取）：MYSQL默认隔离级别
 *        一个事务在执行过程中，可以访问其他事务成功提交的新插入的数据，但不可以访问成功修改的数据。读取数据的事务将会禁止写事务（但允许读事务），写事务则禁止任何其他事务。此隔离级别可有效防止不可重复读和脏读。
 *   Serializable（可串行化）：
 *        提供严格的事务隔离。它要求事务序列化执行，事务只能一个接着一个地执行，不能并发执行。此隔离级别可有效防止脏读、不可重复读和幻读。但这个级别可能导致大量的超时现象和锁竞争，在实际应用中很少使用。
 *
 * Spring事务传播范围：在TransactionDefinition接口中定义了七个事务传播行为
 *   PROPAGATION_REQUIRED：
 *      默认的spring事务传播级别，使用该级别的特点是，如果上下文中已经存在事务，那么就加入到事务中执行，如果当前上下文中不存在事务，则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
 *   PROPAGATION_SUPPORTS：
 *      从字面意思就知道，supports，支持，该传播级别的特点是，如果上下文存在事务，则支持事务加入事务，如果没有事务，则使用非事务的方式执行。所以说，并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
 *   PROPAGATION_MANDATORY：
 *      该级别的事务要求上下文中必须要存在事务，否则就会抛出异常！配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行，但是一旦被调用，就必须有事务包含的情况，就可以使用这个传播级别。
 *   PROPAGATION_REQUIRES_NEW：
 *      从字面即可知道，new，每次都要一个新事务，该传播级别的特点是，每次都会新建一个事务，并且同时将上下文中的事务挂起，执行当前新建事务完成以后，上下文事务恢复再执行。
 *   PROPAGATION_NOT_SUPPORTED：
 *      这个也可以从字面得知，not supported ，不支持，当前级别的特点就是上下文中存在事务，则挂起事务，执行当前逻辑，结束后恢复上下文的事务。
 *   PROPAGATION_NEVER：
 *      该事务更严格，上面一个事务传播级别只是不支持而已，有事务就挂起，而PROPAGATION_NEVER传播级别要求上下文中不能存在事务，一旦有事务，就抛出runtime异常，强制停止执行！
 *   PROPAGATION_NEVER：
 *      该事务更严格，上面一个事务传播级别只是不支持而已，有事务就挂起，而PROPAGATION_NEVER传播级别要求上下文中不能存在事务，一旦有事务，就抛出runtime异常，强制停止执行！
 *   PROPAGATION_NESTED：
 *      字面也可知道，nested，嵌套级别事务。该传播级别特征是，如果上下文中存在事务，则嵌套事务执行，如果不存在事务，则新建事务。
 *
 *
 * 异步事务控制：
 * 1.asyncUpdateUser内异步调用addUser，addUser需要被事务控制。
 *   由于addUser是由异步执行的，所以就算方法asyncUpdateUser有事务注解也不会影响到addUser方法，除非线程等待addUser执行完成
 *   解决办法：1.获取代理对象去调用
 *           2.开启配置，暴露代理对象。是由AopContext获取当前代理对象在去调用
 *
 *  事务方法内使用 synchronized 关键字无法保证并发
 *
 * @author Wangzhiwen
 */
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserRepository userRepository;

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource(name = "custom-thread-pool")
    private ThreadPoolTaskExecutor poolTaskExecutor;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateUser() {
        User man = new User();
        man.setAge(10);
        man.setSex("男");
        userRepository.save(man);
        User woman = new User();
        woman.setAge(18);
        woman.setSex("女");
        userRepository.save(woman);
        // addUser();
        /* updateUser无事务时解决方式二 */
        CustomApplicationContext.getBean(UserService.class).addUser();
        /* updateUser无事务时解决方式三 */
        ((UserService) AopContext.currentProxy()).addUser();
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class/*, propagation = Propagation.REQUIRES_NEW*/)
    public boolean addUser() {
        User user = new User();
        user.setAge(90);
        user.setSex("中");
        userRepository.save(user);
        throw new RuntimeException("子事务异常");
    }

    /**
     * 没有被事务包裹
     * 方法内部异步去调用方法 addUser（有被事务包裹）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean asyncUpdateUser() {
        User man = new User();
        man.setAge(10);
        man.setSex("男");
        userRepository.save(man);
        User woman = new User();
        woman.setAge(18);
        woman.setSex("女");
        userRepository.save(woman);
        /* 此时调用addUser的对象是原始对象，所以addUser内部的事务不会生效 */
        /* 修改事务传播级别 propagation = Propagation.REQUIRES_NEW 不会生效，本质上还是没有是由代理对象  */
        // poolTaskExecutor.execute(this::addUser);

        /* 获取代理对象去调用 */
        // poolTaskExecutor.execute(() -> {
        //     UserService userService = CustomApplicationContext.getBean(UserService.class);
        //     userService.addUser();
        // });

        /*
         *  主线程去控制子线程 主线程有事务包裹  submit.get() 会等待异步线程执行完毕
         *  问题：经测试发现此时，addUser 方法发生异常，此时当前方法事务会回滚，而addUser内的数据不会被回滚
         *  原因：本质上是Spring事务管理是需要绑定当前线程的，子线程不在当前事务绑定的线程中.
         *
         * 尝试方案：把子线程加入到主线程中
         */
        Future<Boolean> submit = poolTaskExecutor.submit(this::addUser);
        try {
            return submit.get();
        } catch (InterruptedException | ExecutionException e) {
            /* 异常被cache时，事务回滚不会生效 */
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return false;
        }
    }

    public void user() {
        String sex = jdbcTemplate.queryForObject("SELECT sex FROM user WHERE id = 1", ResultSet::getString);
        System.out.println(sex);
    }
}
